#include <Ice/Application.h>
#include <IceUtil/Thread.h>
#include <IceGrid/Registry.h>
#include <Glacier2/Glacier2.h>
#include <Yafridng.h>
#include <Magick++.h>
#include <FileStoreI.h>
#include <P2PBrokerI.h>
#include <YafridngRenderManagerI.h>
#include <ResultManager.h>
#include <math.h>
#include <list>

#define STRLEN 254
#define ALLOCATION_TIMEOUT 500

using namespace std;
using namespace Yafridng;
using namespace Magick;

/* Class SessionRefreshThread: This class keep the stablished session alive. If the client
   dies, the session will be closed automatically by the sessionManager. */
class SessionRefreshThread : public IceUtil::Thread, public IceUtil::Monitor<IceUtil::Mutex>
{
public:
  
  SessionRefreshThread(const IceUtil::Time& timeout, const Yafridng::RenderSessionPrx& session) :
    _session(session),
    _timeout(timeout),
    _destroyed(false)
  {
  }
  
  virtual void
  run()
  {
    Lock sync(*this);
    while(!_destroyed)
      {
	timedWait(_timeout);
	if(!_destroyed)
	  {
	    try
	      {
		_session->keepAlive();
	      }
	    catch(const Ice::Exception& ex)
	      {
		cerr << "SessionRefreshThread: " << ex;
		_destroyed = true;
	      }
	  }
      }
  }
  
  void
  destroy()
  {
    Lock sync(*this);
    _destroyed = true;
    notify();
  }
  
private:  
  const Yafridng::RenderSessionPrx _session;
  const IceUtil::Time _timeout;
  bool _destroyed;
};
typedef IceUtil::Handle<SessionRefreshThread> SessionRefreshThreadPtr;

/* Class ResultDispatchThread: This class manages incoming tasks for obtaining result files
   and composing them with the result image. */
class ResultDispatchThread : public IceUtil::Thread
{
public:
  ResultDispatchThread(ResultManagerPtr& resultManager) : 
    _resultManager(resultManager)
  {
  }

  virtual void run()
  {
    bool finish = false;
    while(!finish){
      finish = _resultManager->perform();
      sleep(1);
    }
  }
  
private:
  ResultManagerPtr _resultManager;
};
typedef IceUtil::Handle<ResultDispatchThread> ResultDispatchThreadPtr;

/* Class PingNodesThread: This class makes pings to Nodes to check if they are alive. If they
   are not the zone being rendered by the Node will be stablished to NOT_RENDERED. */
class PingNodesThread : public IceUtil::Thread
{
public:
  PingNodesThread(vector<YafridngRendererPrx> renderers, 
		  YafridngRenderManagerIPtr& renderManager,
		  ResultManagerPtr& resultManager) :
    _renderers(renderers),
    _renderManager(renderManager),
    _resultManager(resultManager)
  {
  }

  virtual void run()
  {
    while(!_renderers.empty())
      {
	vector<YafridngRendererPrx>::iterator p = _renderers.begin();
	while(p != _renderers.end())
	  {
	    //Trying to contact the node
	    try
	      {
		(*p)->ice_ping();
		++p;
	      }

	    //If the node is not up, we cancel its tasks
	    catch(const Ice::Exception& ex)
	      {
		_renderManager->cancelRender((*p)->ice_getIdentity().name);
		p = _renderers.erase(p);
	      }
	  }
	sleep(1);
      }
    
    if(!_resultManager->getDestroy()){
      _resultManager->destroy();
    }
  }

private:
  vector<YafridngRendererPrx> _renderers;
  YafridngRenderManagerIPtr _renderManager;
  ResultManagerPtr _resultManager;
};
typedef IceUtil::Handle<PingNodesThread> PingNodesThreadPtr;

class Renderer : public IceUtil::Shared
{
public:
  Renderer(string fileName, 
	   TZones zones,
	   YafridngRendererPrx& renderer, 
	   int piece,
	   Pieces pieces,
	   int fileSize,
	   FilePrx file,
	   P2PBrokerPrx& broker,
	   YafridngRenderManagerPrx& manager): _fileName(fileName),
					       _zones(zones),
					       _renderer(renderer),
					       _piece(piece),
					       _pieces(pieces),
					       _fileSize(fileSize),
					       _file(file),
					       _broker(broker),
					       _manager(manager)
  {
  }

  void run(){
    try{
      IceUtil::Time p2pTime = IceUtil::Time::now();
      _renderer->obtainFile(_pieces, _piece, _fileSize, _file, _broker);
      IceUtil::Time finalTime = IceUtil::Time::now() - p2pTime;
      cout << endl << "p2pTime Node: " << _renderer->ice_getIdentity().name << "-> " << finalTime.toSecondsDouble() << endl;
      _renderer->render(_zones, _manager);
    }
    catch(const Yafridng::RenderingFailedException& ex)
      {
	cout << "Exception while rendering:" << ex << endl;
	exit(1);
      }
    catch(const Ice::Exception& ex)
      {
      }
  }

private:
  string _fileName;
  TZones _zones;
  YafridngRendererPrx _renderer;
  int _fileSize;
  FilePrx _file;
  int _piece;
  Pieces _pieces;
  P2PBrokerPrx _broker;
  YafridngRenderManagerPrx _manager;
};
typedef IceUtil::Handle<Renderer> RendererPtr;

class RenderQueue : public IceUtil::Monitor<IceUtil::Mutex>, public IceUtil::Shared
{
public:

  RenderQueue(int);
  
  void add(const RendererPtr& renderer);
  void waitForComplete();
  void destroy();
  RendererPtr get();
  
private:
  
  list<IceUtil::ThreadPtr> _workers;
  list<RendererPtr> _work;
  bool _destroy;
};

typedef IceUtil::Handle<RenderQueue> RenderQueuePtr;

class RenderThread : public IceUtil::Thread
{
public:
  
  RenderThread(const RenderQueuePtr& queue) :
    _queue(queue)
  {
  }
  
  virtual void
  run()
  {
    for(;;)
      {
	RendererPtr work = _queue->get();
	if(!work)
	  {
	    return;
	  }
	work->run();
      }
  }
private:
  const RenderQueuePtr _queue;
};

RenderQueue::RenderQueue(int nworkers) :
    _destroy(false)
{
  for(int i = 0; i < nworkers; ++i)
    {
      IceUtil::ThreadPtr t = new RenderThread(this);
      _workers.push_back(t);
      t->start();
    }
}

void
RenderQueue::add(const RendererPtr& renderer)
{
    Lock sync(*this);
    /*IceUtil::ThreadPtr t = new RenderThread(this);
    _workers.push_back(t);
    t->start();*/
    _work.push_back(renderer);
    notify();
}

void
RenderQueue::waitForComplete()
{
  Lock sync(*this);
  while(!_work.empty())
    {
      wait();
    }
}

void
RenderQueue::destroy()
{
    list<IceUtil::ThreadPtr> workers;
    {
	Lock sync(*this);
	workers = _workers;
	_workers.clear();
	_destroy = true;
	notifyAll();
    }

    for(list<IceUtil::ThreadPtr>::const_iterator p = workers.begin(); p != workers.end(); ++p)
    {
	(*p)->getThreadControl().join();
    }
}

RendererPtr
RenderQueue::get()
{
    Lock sync(*this);
    while(_work.empty() && !_destroy)
    {
	wait();
    }

    if(_destroy)
    {
	return 0;
    }

    RendererPtr work = _work.front();
    _work.pop_front();
    notifyAll(); // This is wakes the thread calling waitForComplete().
    return work;
}

#define SCENE 0
#define ANIMATION 1

typedef struct
{
  string fileName;
  int sceneType;
  int nZones;
  int startFrame;
  int endFrame;
  int renderEngine;
  int transferSize;
  int width;
  int height;
  int renderers;
}appInfo;

class YafridngClient
  : virtual public Ice::Application
{
public:
  int getFileSize(string file){
    FILE *fp;
    int size;
    if((fp = fopen(file.c_str(), "rb")) == NULL){
      cout << "Error Opening the file " << file << endl;
      exit(1);
    }
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    cout << "Size of the file: " << size << endl;
    fclose(fp);
    return size;
  }

  void calculatePieces(int fileSize, int nodes, Pieces& out)
  {
    int offset = 0;
    int pieceSize;

    if(nodes > 1)
      pieceSize = fileSize/nodes;
    else
      pieceSize = fileSize;

    for(int i = 0; i < nodes; i++){
      Piece p;
      if((offset + (2*pieceSize)) > fileSize)
	pieceSize += fileSize%nodes;
      p.offset = offset;
      p.pieceSize = pieceSize;
      out.push_back(p);
      offset += pieceSize;
    }
  }

  void calculateZones(appInfo info, TZones& zones){
    if(info.sceneType == SCENE){
      int hsize, vsize, x1, y1, x2, y2, id;
      int l = (int) pow(info.nZones, 2);
      hsize = info.width/info.nZones;
      vsize = info.height/info.nZones;
      
      cout << "Calculating work units..." << endl;
      cout << "NUMBER OF ZONES: " << l << endl;
      id = 0;
      for(int i = 0; i < info.nZones; i++){
	x1 = 0;
	y1 = vsize * i;
	x2 = x1 + hsize;
	y2 = y1 + vsize;
	if(y2 + vsize > info.height)
	  y2 += info.height%l;
	for(int j = 0; j < info.nZones; j++){
	  TZone temp;
	  temp.id = id;
	  temp.frame = 1;
	  temp.x1 = x1;
	  temp.y1 = y1;
	  temp.x2 = x2;
	  temp.y2 = y2;
	  x1 += hsize;
	  x2 += hsize;
	  if(x2 + hsize > info.width)
	    x2 += info.width%info.nZones;
	  zones.push_back(temp);
	  id++;
	}
      }
    }
    else{
      int id = 0;
      int l = info.endFrame - info.startFrame;
      int frame = info.startFrame;
      for(int i = 0; i < l; i++){
	TZone temp;
	temp.id = id;
	temp.frame = frame;
	temp.x1 = 0;
	temp.y1 = 0;
	temp.x2 = info.width;
	temp.y2 = info.height;
	zones.push_back(temp);
	frame++;
	id++;
      }
    }
  }

  appInfo parseLine(int argc, char *argv[])
  {
    appInfo info;
    FILE* fp;
    char temp[STRLEN];

    if(!argv[1]){
      cout << "No file Specified" << endl;
      exit(1);
    }
    info.fileName = argv[1];
    info.sceneType = SCENE;
    info.startFrame = 1;
    info.endFrame = 1;

    if(argv[2])
      info.renderers = atoi(argv[2]);

    else{
      cout << "Missing parameter: number of renderers. Terminating Yafridng." << endl;
      exit(1);
    }

    if(argv[3])
      info.transferSize = atoi(argv[3]);

    else{
      cout << "Missing parameter: transfer unit size. Terminating Yafridng." << endl;
      exit(1);
    } 

    if(argv[4]){
      if(!strcmp(argv[4], "-a")){
	info.sceneType = ANIMATION;
	info.nZones = 0;
	if(argv[5] && argv[6]){
	  info.startFrame = atoi(argv[5]);
	  info.endFrame = atoi(argv[6]);
	}

	else{
	  cout << "Missing parameter: startFrame or endFrame. Terminating Yafridng." << endl;
	  exit(1);
	}
      }

      else
	info.nZones = atoi(argv[4]);
    }

    else
      info.nZones = 3;

    string command = "blender " + info.fileName + " -b -P getRes.py";
    //Obtainig resolution of the scene
    system(command.c_str());
    fp = fopen("scene.txt", "r");
    fgets(temp, STRLEN, fp);
    info.renderEngine = atoi(temp);
    fgets(temp, STRLEN, fp);
    info.width = atoi(temp);
    fgets(temp, STRLEN, fp);
    info.height = atoi(temp);
    fclose(fp);

    return info;
  }
  
  virtual int run (int argc,
		   char *argv[])
  {
    appInfo info = parseLine(argc, argv);
    TZones zones;
    Pieces pieces;
    vector<YafridngRendererPrx> renderers;
    string fileName = info.fileName;
    string path;
    string result = fileName.substr(0, fileName.find(".", 0));
    cout << "Scene name: " << info.fileName << endl;
    cout << "Zones: " << info.nZones << endl;
    cout << "Renderers: " << info.renderers << endl << endl;
    cout << "Scene resolution: " << endl;
    cout << "\twidth: " << info.width << endl;
    cout << "\theigh: " << info.height << endl << endl;

    if(info.sceneType == SCENE)
      {
	result += ".png";
	path = "./temp/";
	Color transp(MaxRGB, MaxRGB, MaxRGB, MaxRGB);
	Magick::Image empty_image(Geometry(info.width, info.height), transp);
	empty_image.write(result);
      }
    
    else
      {
	string mkdir = "mkdir ";
	mkdir += result;
	system(mkdir.c_str());
	path = "./" + result + "/";
      }
    calculateZones(info, zones);

    Ice::RouterPrx defaultRouter = communicator()->getDefaultRouter();
    if(!defaultRouter)
      {
	cerr << argv[0] << ": no default router stablished" << endl;
	return EXIT_FAILURE;
      }

    Glacier2::RouterPrx router = Glacier2::RouterPrx::checkedCast(defaultRouter);
    if(!router)
      {
	cerr << argv[0] << ": configured router is not a glacier2 router" << endl;
	return EXIT_FAILURE;
      }

    Yafridng::RenderSessionPrx session;
    try{
      session = Yafridng::RenderSessionPrx::uncheckedCast(router->createSessionFromSecureConnection());
    }

    catch(const IceGrid::PermissionDeniedException& ex){
      cout << "Permission denied:\n" << ex.reason << endl;
      return 1;
    }

    SessionRefreshThreadPtr refresh =
      new SessionRefreshThread(IceUtil::Time::seconds(router->getSessionTimeout()/2),
			       session);
    refresh->start();

    bool end = false;
    int fileSize = getFileSize(fileName);
    int num = info.transferSize;
    int allocatedRenderers = 0;
    /* Obtaining resources for rendering */
    while(!end)
      {
	try
	  {
	    Ice::ObjectPrx obj = session->create(info.renderEngine, info.fileName);
	    YafridngRendererPrx renderer = YafridngRendererPrx::checkedCast(obj);
	    renderers.push_back(renderer);
	    allocatedRenderers++;
	    if(allocatedRenderers == info.renderers)
	      end = true;
	  }
	//If there are not more nodes
	catch(Yafridng::NoMoreNodesException& ex)
	  {
	    cout << "No more allocatable rendererFactories" << endl;
	    end = true;
	  }
	//If a node is not executing
	catch(Ice::UnknownLocalException& ex)
	  {
	    cout << "Not working node." << ex << endl;
	    end = true;
	  }
      }

    cout << "Allocated Renderers: " << allocatedRenderers << endl;
    RenderQueuePtr queue = new RenderQueue(allocatedRenderers);

    //Obtaining category for bidirectional connections
    string category = router->getServerProxy()->ice_getIdentity().category;
    Ice::ObjectAdapterPtr adapter = communicator()->createObjectAdapter("YafridngClientAdapter");
    if(!renderers.empty()){
      Identity renderManagerIdentity;
      renderManagerIdentity.name = "ResultManager";
      renderManagerIdentity.category = category;
      ResultManagerPtr resultManager = new ResultManager(result, info.sceneType);
      YafridngRenderManagerIPtr renderManager = new YafridngRenderManagerI(info.fileName,
									   path,
									   info.width,
									   info.height,
									   zones,
									   resultManager);
      Ice::ObjectPrx managerobj = adapter->add(renderManager,
					       renderManagerIdentity);
      YafridngRenderManagerPrx manager = YafridngRenderManagerPrx::uncheckedCast(managerobj);

      Identity fileStoreIdentity;
      fileStoreIdentity.name = "FileStore";
      fileStoreIdentity.category = category;
      Ice::ObjectPrx fileobj = adapter->add(new FileStoreI(category), 
					    fileStoreIdentity);

      FileStorePrx file = FileStorePrx::uncheckedCast(fileobj);
      calculatePieces(fileSize, renderers.size(), pieces);
      Identity p2pBrokerIdentity;
      p2pBrokerIdentity.name = "p2pBroker";
      p2pBrokerIdentity.category = category;

      Ice::ObjectPrx broker = adapter->add(new P2PBrokerI(info.fileName, file, pieces),
					   p2pBrokerIdentity);
      Yafridng::P2PBrokerPrx p2pBroker = Yafridng::P2PBrokerPrx::uncheckedCast(broker);
      adapter->activate();

      //Creating PingNodesThread
      PingNodesThreadPtr pingNodes = new PingNodesThread(renderers, renderManager, resultManager);
      pingNodes->start();
      
      ResultDispatchThreadPtr resultDispatch = new ResultDispatchThread(resultManager);
      resultDispatch->start();

      IceUtil::Time start = IceUtil::Time::now();
      
      /* Adding renderers to the queue. A thread will be executed
	 for each of the renderers */
      for(int i = 0; i < renderers.size(); i++){
	queue->add(new Renderer(info.fileName, 
				zones, 
				renderers.at(i), 
				i, 
				pieces,
				fileSize,
				file->read(fileName, num, pieces.at(i).offset, pieces.at(i).pieceSize),
				p2pBroker,
				manager));
      }
      queue->waitForComplete();
      queue->destroy();
      
      resultDispatch->getThreadControl().join();

      //Releasing renderers
      for(int i = 0; i < renderers.size(); i++){
	try
	  {
	    renderers.at(i)->flush();
	  }
	catch(Ice::Exception& ex)
	  {
	    //Blank Exception
	  }
      }

      pingNodes->getThreadControl().join();

      IceUtil::Time tm = IceUtil::Time::now() - start;
      cout << endl << "FINAL TIME: " << tm.toSecondsDouble() << endl;
    }

    else
      cout << "No renderers available. Try it later" << endl;

    refresh->destroy();
    refresh->getThreadControl().join();
    session->destroy();
    adapter->destroy();
    return 0;
  }
};

int main(int argc,
	 char* argv[])
{
  YafridngClient client;
  return client.main(argc, argv, "client.cfg");
}
